/**
 * Copyright (C) 2011 Angelo Zerr <angelo.zerr@gmail.com> and Pascal Leclercq <pascal.leclercq@gmail.com>
 *
 * All rights reserved.
 *
 * Permission is hereby granted, free  of charge, to any person obtaining
 * a  copy  of this  software  and  associated  documentation files  (the
 * "Software"), to  deal in  the Software without  restriction, including
 * without limitation  the rights to  use, copy, modify,  merge, publish,
 * distribute,  sublicense, and/or sell  copies of  the Software,  and to
 * permit persons to whom the Software  is furnished to do so, subject to
 * the following conditions:
 *
 * The  above  copyright  notice  and  this permission  notice  shall  be
 * included in all copies or substantial portions of the Software.
 *
 * THE  SOFTWARE IS  PROVIDED  "AS  IS", WITHOUT  WARRANTY  OF ANY  KIND,
 * EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF
 * MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
 * OF CONTRACT, TORT OR OTHERWISE,  ARISING FROM, OUT OF OR IN CONNECTION
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
package org.apache.struts2.views.xdocreport;

import static fr.opensagres.xdocreport.core.utils.StringUtils.FALSE;
import static fr.opensagres.xdocreport.core.utils.StringUtils.TRUE;
import static fr.opensagres.xdocreport.core.utils.StringUtils.asBoolean;
import static fr.opensagres.xdocreport.core.utils.StringUtils.isEmpty;
import static fr.opensagres.xdocreport.core.utils.StringUtils.isNotEmpty;
import static org.apache.struts2.views.xdocreport.ActionInvocationUtils.getRequest;
import static org.apache.struts2.views.xdocreport.ActionInvocationUtils.getResponse;
import static org.apache.struts2.views.xdocreport.ActionInvocationUtils.getServletContext;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts2.dispatcher.StrutsResultSupport;

import com.opensymphony.xwork2.ActionInvocation;

import fr.opensagres.xdocreport.converter.ConverterTypeTo;
import fr.opensagres.xdocreport.converter.IConverter;
import fr.opensagres.xdocreport.converter.IURIResolver;
import fr.opensagres.xdocreport.converter.MimeMapping;
import fr.opensagres.xdocreport.converter.Options;
import fr.opensagres.xdocreport.converter.XDocConverterException;
import fr.opensagres.xdocreport.core.XDocReportException;
import fr.opensagres.xdocreport.core.logging.LogUtils;
import fr.opensagres.xdocreport.core.utils.StringUtils;
import fr.opensagres.xdocreport.document.IXDocReport;
import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
import fr.opensagres.xdocreport.document.web.WEBURIResolver;
import fr.opensagres.xdocreport.template.IContext;
import fr.opensagres.xdocreport.template.TemplateEngineKind;
import fr.opensagres.xdocreport.template.formatter.FieldsMetadata;

/**
 * Abstract class for manage Struts2 Result with XDocReport to generate odt,
 * docx report by using odt, docx document and convert it to another format like
 * PDF/XHTML.
 * 
 */
public abstract class AbstractXDocReportResult extends StrutsResultSupport {

	private static final long serialVersionUID = -3844927561499091875L;
	/**
	 * Logger for this class
	 */
	private static final Logger LOGGER = LogUtils.getLogger(AbstractXDocReportResult.class);


	// HTTP Header constants
	private static final String SAT_6_MAY_1995_12_00_00_GMT = "Sat, 6 May 1995 12:00:00 GMT";
	private static final String EXPIRES = "Expires";
	private static final String POST_CHECK_0_PRE_CHECK_0 = "post-check=0, pre-check=0";
	private static final String NO_CACHE = "no-cache";
	private static final String PRAGMA = "Pragma";
	private static final String NO_STORE_NO_CACHE_MUST_REVALIDATE = "no-store, no-cache, must-revalidate";
	private static final String CACHE_CONTROL_HTTP_HEADER = "Cache-Control";

	// Content-Disposition HTTP response Header
	private static final String CONTENT_DISPOSITION_HEADER = "Content-Disposition";
	private static final String ATTACHMENT_FILENAME = "attachment; filename=\"";

	private static final String WEB_URI_RESOLVER_DATA_KEY = WEBURIResolver.class
			.getName();

	protected static final String ACTION_KEY = "#action";

	private static final Map<Class, Boolean> isXDocReportInitializerAwareClassCache = new HashMap<Class, Boolean>();

	public static final String LAST_MODIFIED = AbstractXDocReportResult.class
			.getSimpleName() + "_LAST_MODIFIED";

	private String templateEngine = TemplateEngineKind.Velocity.name();
	private String converter = null;
	private String[] expressions = { ACTION_KEY };
	private String trackLastModified = FALSE;
	private String download = TRUE;
	private String fieldsAsList = null;

	/**
	 * Set Template engine to use (according the JAR which is added in the
	 * classpath, values are Velocity|Freemarker). The template engine can use
	 * OGNL expression too (ex : #action.templateEngine will use the
	 * Action#getTemplateEngine() method).
	 * 
	 * @param templateEngine
	 */
	public void setTemplateEngine(String templateEngine) {
		this.templateEngine = templateEngine;
	}

	/**
	 * Get Template engine to use (according the JAR which is added in the
	 * classpath, values are Velocity|Freemarker).
	 * 
	 * @return
	 */
	public String getTemplateEngine() {
		return templateEngine;
	}

	/**
	 * Set the converter to use (according the JAR which is added in the
	 * classpath, values are static value PDF_ITEXT|XHTML_XWPF ). The converter
	 * can use OGNL expression too (ex : #action.converter will use the
	 * Action#getConverter() method). If converter is null, no conversion is
	 * done.
	 * 
	 * @param converter
	 */
	public void setConverter(String converter) {
		this.converter = converter;
	}

	/**
	 * Returns the converter to use (according the JAR which is added in the
	 * classpath, values are PDF_ITEXT|XHTML_XWPF). If converter is null, no
	 * conversion is done.
	 * 
	 * @return
	 */
	public String getConverter() {
		return converter;
	}

	/**
	 * 
	 * @param expression
	 */
	public void setExpression(String expression) {
		this.expressions = expression.split(",");
	}

	/**
	 * 
	 * @return
	 */
	public String[] getExpressions() {
		return expressions;
	}

	/**
	 * Set to true if docx, odt file which is used to load must be tracked to
	 * observe the document change. If document change, report will be reloaded
	 * with the new file.
	 * 
	 * @param trackLastModified
	 */
	public void setTrackLastModified(String trackLastModified) {
		this.trackLastModified = trackLastModified;
	}

	/**
	 * Returns true if docx, odt file which is used to load must be tracked to
	 * observe the document change and false otherwise. If document change,
	 * report will be reloaded with the new file.
	 * 
	 * @return
	 */
	public String getTrackLastModified() {
		return trackLastModified;
	}

	/**
	 * Set to true if report must be downloaded (generate
	 * Content-Disposition:"attachment; filename... in the HTTP Header) and
	 * false otherwise. By default download is true.
	 * 
	 * @param download
	 */
	public void setDownload(String download) {
		this.download = download;
	}

	/**
	 * Returns true if report must be downloaded (generate
	 * Content-Disposition:"attachment; filename... in the HTTP Header) and
	 * false otherwise.
	 * 
	 * @return
	 */
	public String getDownload() {
		return download;
	}

	public void setFieldAsList(String fieldAsList) {
		this.fieldsAsList = fieldAsList;
	}

	public String[] getFieldsAsList() {
		if (isEmpty(fieldsAsList)) {
			return StringUtils.EMPTY_STRING_ARRAY;
		}
		return fieldsAsList.split(",");
	}

	@Override
	protected void doExecute(String finalLocation, ActionInvocation invocation)
			throws Exception {

		String location = getLocation(finalLocation, invocation);
		long startTime = -1;
		if(LOGGER.isLoggable(Level.FINE))
		{
			startTime = System.currentTimeMillis();
			
			LOGGER.fine(String.format("Start XDocReportResult for location=%s, templateEngine %s",location, getTemplateEngine()));
		}
		
		try {

			// 1) Get or load Report from the registry
			XDocReportRegistry registry = XDocReportRegistry.getRegistry();
			IXDocReport report = getReport(registry, location, invocation);
			if (report == null) {
				throw new XDocReportException(
						"Cannot get XDoc Report for location=" + location);
			}

			// 2) Populate Java model context with Struts2 value Stack
			IContext context = report.createContext();
			populateContext(report, context, location, invocation);

			// 3) Process or convert the report
			Options options = getOptionsConverter(report, location, invocation);
			if (options == null) {
				doProcessReport(report, context, location, invocation);
			} else {
				doProcessReportWithConverter(report, context, options,
						location, invocation);
			}

			if(LOGGER.isLoggable(Level.FINE))
			{
				startTime = System.currentTimeMillis();
				LOGGER.fine(String.format("End XDocReportResult with SUCCESS for location=%s, templateEngine %s  done in %s ms",location, getTemplateEngine(),(System.currentTimeMillis() - startTime)));
			}
			
		} catch (Throwable e) {
			if(LOGGER.isLoggable(Level.FINE))
			{
				startTime = System.currentTimeMillis();
				LOGGER.fine(String.format("End XDocReportResult with SUCCESS for location=%s, templateEngine %s  done in %s ms",location, getTemplateEngine(),(System.currentTimeMillis() - startTime)));
				LOGGER.throwing(getClass().getName(), "doExecute", e);
			}
			if (e instanceof Exception) {
				throw (Exception) e;
			}
			throw new ServletException(e);
		}
	}

	// ------------------------------ Get Report
	/**
	 * 
	 * @param registry
	 * @param finalLocation
	 * @param invocation
	 * @return
	 * @throws ServletException
	 * @throws IOException
	 */
	protected IXDocReport getReport(XDocReportRegistry registry,
			String location, ActionInvocation invocation)
			throws ServletException, IOException {

		boolean trackLastModified = isTrackLastModified(invocation);
		// 1) Test if report is already registered in the registry.
		String reportId = getReportId(location);
		IXDocReport report = registry.getReport(reportId);
		if (report != null && !trackLastModified) {
			return report;
		}

		// 2) Compute location type
		LocationType locationType = LocationType.getLocationType(location);
		if (report != null) {
			// Here report was already loaded and track last modified must be
			// done.
			if (locationType == LocationType.CLASSPATH) {
				// Source stream comes from classpath, returns the loaded
				// report.
				return report;
			}

		}

		// 3) Get the source file
		File sourceFile = getSourceFile(location, locationType, invocation);
		if (report != null) {
			if (sourceFile == null) {
				// Here report was already loaded but source file is not
				// available
				// (comes from CLASSPATH), returns the loaded report.
				return report;
			}
			// Here report was already loaded and source file is
			// available, compare last modified
			Long lastModified = report.getData(LAST_MODIFIED);
			if (lastModified == null) {
				return report;
			}
			if (lastModified >= sourceFile.lastModified()) {
				// No modification about source file.
				return report;
			}
			// Source file was modified after load report, remove the report
			// from the registry to force the load.
			registry.unregisterReport(reportId);
		}

		// 2) Get source stream
		InputStream sourceStream = getSourceStream(location, locationType,
				sourceFile, invocation);
		if (sourceStream == null) {
			throw new IOException("Stream null for location=" + location);
		}

		try {
			report = registry.loadReport(sourceStream, reportId,
					getTemplateEngine(invocation));
			report.setFieldsMetadata(getFieldsMetadata(report, location,
					invocation));
			Object action = invocation.getStack().findValue(ACTION_KEY);
			XDocReportInitializerAware initializer = getXDocReportInitializerAware(action);
			if (initializer != null) {
				initializer.initialize(report);
			}
			report.setData(LAST_MODIFIED, System.currentTimeMillis());
			return report;
		} catch (IOException e) {
			throw new ServletException("Inputstream", e);
		} catch (XDocReportException e) {
			throw new ServletException("Inputstream", e);
		}
	}

	protected boolean isTrackLastModified(ActionInvocation invocation) {
		String trackLastModified = getTrackLastModified();
		trackLastModified = getValue(trackLastModified, invocation);
		return asBoolean(trackLastModified, false);
	}

	protected boolean isDownload(ActionInvocation invocation) {
		String download = getDownload();
		download = getValue(download, invocation);
		return asBoolean(download, true);
	}

	protected String getLocation(String finalLocation,
			ActionInvocation invocation) {
		return getValue(finalLocation, invocation);
	}

	protected String getTemplateEngine(ActionInvocation invocation) {
		String templateEngine = getTemplateEngine();
		return getValue(templateEngine, invocation);
	}

	protected String getValue(String value, ActionInvocation invocation) {
		if (isEmpty(value)) {
			return value;
		}
		if (value.startsWith("#")) {
			return invocation.getStack().findString(value);
		}
		return value;
	}

	protected String getReportId(String finalLocation) {
		StringBuilder reportId = new StringBuilder(StringUtils.replaceAll(
				finalLocation, "/", "_"));
		reportId.append("_");
		reportId.append(getTemplateEngine());
		return reportId.toString();
	}

	protected InputStream getSourceStream(final String finalLocation,
			LocationType locationType, File sourceFile,
			ActionInvocation invocation) throws IOException {
		switch (locationType) {
		case CLASSPATH:
			String location = locationType.getLocation(finalLocation);
			return getSourceStreamFromClasspath(location);
		case FILESYSTEM:
			return new FileInputStream(sourceFile);
		default:
			return new FileInputStream(sourceFile);
		}
	}

	protected File getSourceFile(final String finalLocation,
			LocationType locationType, ActionInvocation invocation) {
		String location = locationType.getLocation(finalLocation);
		switch (locationType) {
		case CLASSPATH:
			return null;
		case FILESYSTEM:
			return new File(location);
		default:
			ServletContext servletContext = getServletContext(invocation);
			return new File(servletContext.getRealPath(location));
		}
	}

	protected InputStream getSourceStreamFromClasspath(String path) {
		ClassLoader cl = this.getClass().getClassLoader();
		if (cl == null) {
			// no class loader specified -> use thread context class loader
			cl = Thread.currentThread().getContextClassLoader();
		}
		return cl.getResourceAsStream(path);
	}

	protected FieldsMetadata getFieldsMetadata(IXDocReport report,
			String location, ActionInvocation invocation) {
		String[] fieldsAsList = getFieldsAsList();
		if (fieldsAsList != null && fieldsAsList.length > 0) {
			FieldsMetadata fieldsMetadata = new FieldsMetadata();
			for (int i = 0; i < fieldsAsList.length; i++) {
				fieldsMetadata.addFieldAsList(getValue(fieldsAsList[i],
						invocation));
			}
			return fieldsMetadata;
		}
		return null;
	}

	// --------- Process reportoid

	/**
	 * 
	 * @param report
	 * @param context
	 * @param options
	 * @param finalLocation
	 * @param invocation
	 * @throws IOException
	 * @throws XDocReportException
	 */
	protected void doProcessReport(IXDocReport report, IContext context,
			String finalLocation, ActionInvocation invocation)
			throws XDocReportException, IOException {

		HttpServletResponse response = getResponse(invocation);
		// 2) Prepare HTTP response content type
		prepareHTTPResponse(report.getId(), finalLocation,
				report.getMimeMapping(), invocation, getRequest(invocation),
				response);

		// 2) Generate report
		report.process(context, response.getOutputStream());

	}

	// --------- Converter

	/**
	 * 
	 * @param report
	 * @param context
	 * @param finalLocation
	 * @param invocation
	 * @throws IOException
	 * @throws XDocReportException
	 * @throws XDocConverterException
	 */
	protected void doProcessReportWithConverter(IXDocReport report,
			IContext context, Options options, String finalLocation,
			ActionInvocation invocation) throws XDocConverterException,
			XDocReportException, IOException {

		HttpServletResponse response = getResponse(invocation);

		// 2) Get converter
		IConverter converter = report.getConverter(options);
		// 3) Prepare HTTP response content type
		prepareHTTPResponse(report.getId(), finalLocation,
				converter.getMimeMapping(), invocation, getRequest(invocation),
				response);

		// 4) Generate report with conversion
		report.convert(context, options, response.getOutputStream());

	}

	/**
	 * 
	 * @param report
	 * @param request
	 * @return
	 */
	protected Options getOptionsConverter(IXDocReport report,
			String finalLocation, ActionInvocation invocation) {
		final String converterId = getConverter(invocation);
		if (isEmpty(converterId)) {
			return null;
		}
		Options options = null;
		int index = converterId.lastIndexOf('_');
		if (index != -1) {
			String to = converterId.substring(0, index);
			String via = converterId.substring(index + 1, converterId.length());
			options = Options.getTo(to).via(via);
		} else {
			options = Options.getTo(converterId);
		}
		prepareOptions(options, report, converterId, finalLocation, invocation);
		return options;
	}

	protected String getConverter(ActionInvocation invocation) {
		String converter = getConverter();
		return getValue(converter, invocation);
	}

	protected void prepareOptions(Options options, IXDocReport report,
			String converterId, String finalLocation,
			ActionInvocation invocation) {
		if (ConverterTypeTo.FO.name().equals(options.getTo())
				|| ConverterTypeTo.XHTML.name().equals(options.getTo())) {
			options.setProperty(
					IURIResolver.class.getName(),
					createWEBURIResolver(report, converterId, finalLocation,
							invocation));
		}
	}

	protected IURIResolver createWEBURIResolver(IXDocReport report,
			String converterId, String finalLocation,
			ActionInvocation invocation) {
		WEBURIResolver resolver = report.getData(WEB_URI_RESOLVER_DATA_KEY);
		if (resolver == null) {
			resolver = new WEBURIResolver(report.getId(),
					getRequest(invocation));
			report.setData(WEB_URI_RESOLVER_DATA_KEY, resolver);
		}
		return resolver;
	}

	protected void prepareHTTPResponse(String reportId, String finalLocation,
			MimeMapping mimeMapping, ActionInvocation invocation,
			HttpServletRequest request, HttpServletResponse response) {
		if (mimeMapping != null) {
			response.setContentType(mimeMapping.getMimeType());
		}

		// Check if Content-Disposition must be generated?
		if (isGenerateContentDisposition(reportId, mimeMapping, invocation)) {
			String sourceFileName = finalLocation;
			int index = finalLocation.lastIndexOf('/');
			if (index != -1) {
				sourceFileName = finalLocation.substring(index + 1,
						finalLocation.length());
			}
			String contentDisposition = getContentDisposition(sourceFileName,
					mimeMapping, request);
			if (isNotEmpty(contentDisposition)) {
				response.setHeader(CONTENT_DISPOSITION_HEADER,
						contentDisposition.toString());
			}
		}
		// Disable HTTP response cache
		if (isDisableHTTPResponseCache()) {
			disableHTTPResponseCache(response);
		}
	}

	protected boolean isGenerateContentDisposition(String reportId,
			MimeMapping mimeMapping, ActionInvocation invocation) {
		return isDownload(invocation);
	}

	protected void prepareHTTPResponse(String reportId, String entryName,
			ActionInvocation invocation, HttpServletRequest request,
			HttpServletResponse response) {

		// Check if Content-Disposition must be generated?
		if (isGenerateContentDisposition(reportId, null, invocation)) {
			String contentDisposition = getContentDisposition(entryName);
			if (isNotEmpty(contentDisposition)) {
				response.setHeader(CONTENT_DISPOSITION_HEADER,
						contentDisposition.toString());
			}
		}

		// Disable HTTP response cache
		if (isDisableHTTPResponseCache()) {
			disableHTTPResponseCache(response);
		}
	}

	protected boolean isDisableHTTPResponseCache() {
		return true;
	}

	protected String getContentDisposition(String sourceFileName,
			MimeMapping mimeMapping, HttpServletRequest request) {
		if (mimeMapping != null) {
			String fileName = mimeMapping.formatFileName(sourceFileName);
			return getContentDisposition(fileName);
		}
		return null;
	}

	protected String getContentDisposition(String fileName) {
		StringBuilder contentDisposition = new StringBuilder(
				ATTACHMENT_FILENAME);
		contentDisposition.append(fileName);
		contentDisposition.append("\"");
		return contentDisposition.toString();
	}

	/**
	 * Disable cache HTTP hearder.
	 * 
	 * @param response
	 */
	protected void disableHTTPResponseCache(HttpServletResponse response) {
		// see article http://onjava.com/pub/a/onjava/excerpt/jebp_3/index2.html
		// Set to expire far in the past.
		response.setHeader(EXPIRES, SAT_6_MAY_1995_12_00_00_GMT);
		// Set standard HTTP/1.1 no-cache headers.
		response.setHeader(CACHE_CONTROL_HTTP_HEADER,
				NO_STORE_NO_CACHE_MUST_REVALIDATE);
		// Set IE extended HTTP/1.1 no-cache headers (use addHeader).
		response.addHeader(CACHE_CONTROL_HTTP_HEADER, POST_CHECK_0_PRE_CHECK_0);
		// Set standard HTTP/1.0 no-cache header.
		response.setHeader(PRAGMA, NO_CACHE);
	}

	protected XDocReportInitializerAware getXDocReportInitializerAware(
			Object action) {
		if (action == null) {
			return null;
		}
		Boolean result = isXDocReportInitializerAwareClassCache.get(action
				.getClass());
		if (result == null) {
			result = (action instanceof XDocReportInitializerAware);
			isXDocReportInitializerAwareClassCache.put(action.getClass(),
					result);
		}
		if (result) {
			return ((XDocReportInitializerAware) action);
		}
		return null;
	}

	/**
	 * 
	 * @param report
	 * @param finalLocation
	 * @param invocation
	 * @return
	 * @throws XDocReportException
	 */
	protected abstract void populateContext(IXDocReport report,
			IContext context, String finalLocation, ActionInvocation invocation)
			throws Exception;

}
